package cn.tedu.mall.seckill.service.impl;

import cn.tedu.mall.common.config.PrefixConfiguration;
import cn.tedu.mall.common.exception.CoolSharkServiceException;
import cn.tedu.mall.common.restful.JsonPage;
import cn.tedu.mall.common.restful.ResponseCode;
import cn.tedu.mall.pojo.product.vo.SpuDetailStandardVO;
import cn.tedu.mall.pojo.product.vo.SpuStandardVO;
import cn.tedu.mall.pojo.seckill.model.SeckillSpu;
import cn.tedu.mall.pojo.seckill.vo.SeckillSpuDetailSimpleVO;
import cn.tedu.mall.pojo.seckill.vo.SeckillSpuVO;
import cn.tedu.mall.product.service.seckill.IForSeckillSpuService;
import cn.tedu.mall.seckill.mapper.SeckillSpuMapper;
import cn.tedu.mall.seckill.service.ISeckillSpuService;
import cn.tedu.mall.seckill.timer.job.SeckillBloomInitialJob;
import cn.tedu.mall.seckill.utils.RedisBloomUtils;
import cn.tedu.mall.seckill.utils.SeckillCacheUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.math.RandomUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class SeckillSpuServiceImpl implements ISeckillSpuService {

    // 装配查询秒杀信息的mapper
    @Autowired
    private SeckillSpuMapper seckillSpuMapper;

    // 查询spu常规信息,需要dubbo调用product模块
    @DubboReference
    private IForSeckillSpuService dubboSeckillSpuService;


    // 分页查询秒杀商品信息
    // 返回值SeckillSpuVO是一个既包含spu常规信息,又包含spu秒杀信息的对象
    // 我们需要从mall_seckill数据库获得秒杀信息,从mall_pms数据库获取常规信息
    @Override
    public JsonPage<SeckillSpuVO> listSeckillSpus(Integer page, Integer pageSize) {
        // 设置分页条件
        PageHelper.startPage(page,pageSize);
        // 执行查询秒杀商品信息列表的方法
        List<SeckillSpu> seckillSpus=seckillSpuMapper.findSeckillSpus();
        // 实例化SeckillSpuVO泛型的集合,用户保存商品信息对象,以备后续代码返回
        List<SeckillSpuVO> seckillSpuVOs=new ArrayList<>();
        // 遍历seckillSpus(只有秒杀信息的集合)
        for(SeckillSpu seckillSpu : seckillSpus){
            // 获取spuId
            Long spuId=seckillSpu.getSpuId();
            // 根据spuId,dubbo查询spuId对应的商品常规信息
            SpuStandardVO standardVO=dubboSeckillSpuService.getSpuById(spuId);
            // 实例化SeckillSpuVO对象,用于接收常规信息和秒杀信息
            SeckillSpuVO seckillSpuVO=new SeckillSpuVO();
            // 将常规信息,赋值到seckillSpuVO
            BeanUtils.copyProperties(standardVO,seckillSpuVO);
            // 常规信息赋值完毕,下面把秒杀信息赋值到seckillSpuVO
            seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
            seckillSpuVO.setStartTime(seckillSpu.getStartTime());
            seckillSpuVO.setEndTime(seckillSpu.getEndTime());
            // 到此为止,seckillSpuVO就包含了常规信息和秒杀信息,添加到集合中
            seckillSpuVOs.add(seckillSpuVO);
        }
        // 最后返回分页类型JsonPage的返回值
        return JsonPage.restPage(new PageInfo<>(seckillSpuVOs));
    }

    // 装配操作Redis的对象
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisBloomUtils redisBloomUtils;

    // 根据spuId查询SeckillSpuVO对象
    @Override
    public SeckillSpuVO getSeckillSpu(Long spuId) {
        // 在后面的完整版代码中,我们在这里编写布隆过滤器判断
        // 如果布隆过滤器中不包含参数spuId的值,抛出异常,终止方法,防止缓存穿透
        String bloomDayKey = SeckillCacheUtils.getBloomFilterKey(
                LocalDate.now());
        log.debug("当前批次商品布隆过滤器key为：{}",bloomDayKey);
        //先判断key是否存在
        if (!redisTemplate.hasKey(bloomDayKey)){
            throw new CoolSharkServiceException(ResponseCode.INTERNAL_SERVER_ERROR,
                    "布隆过滤器未创建");
        }
        if (!redisBloomUtils.bfexists(bloomDayKey,spuId+"")){
            throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
                    "您访问的商品不存在！"); }

        // 当前业务是会将从数据库查询出的信息保存到Redis的,所以先检查Redis中有没有
        // mall:seckill:spu:vo:2
        String spuVOKey= SeckillCacheUtils.getSeckillSpuVOKey(spuId);
        // 声明一个目标返回值类型的对象
        SeckillSpuVO seckillSpuVO=null;
        // 判断Redis中是否包含这个key
        if(redisTemplate.hasKey(spuVOKey)){
            // 如果存在,直接从redis中获取,以备返回
            seckillSpuVO= (SeckillSpuVO) redisTemplate
                                   .boundValueOps(spuVOKey).get();
        }else{
            // 如果redis中没有这个数据,就要从数据库查询
            // 要查询秒杀信息和常规信息,最终都赋值到seckillSpuVO中
            SeckillSpu seckillSpu=seckillSpuMapper.findSeckillSpuById(spuId);
            // 判断一下这个seckillSpu是否为null(因为布隆过滤器可能会误判)
            if(seckillSpu == null){
                // 进这个if就是发生了缓存穿透(redis中没有,数据库中也没有)
                throw new CoolSharkServiceException(
                        ResponseCode.NOT_FOUND,"您要访问的商品不存在!");
            }
            // 获取了秒杀信息后,查询常规信息
            SpuStandardVO spuStandardVO =
                            dubboSeckillSpuService.getSpuById(spuId);
            // 将秒杀信息和常规信息都赋值到seckillSpuVO中
            seckillSpuVO=new SeckillSpuVO();
            BeanUtils.copyProperties(spuStandardVO,seckillSpuVO);
            // 手动赋值秒杀信息
            seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
            seckillSpuVO.setStartTime(seckillSpu.getStartTime());
            seckillSpuVO.setEndTime(seckillSpu.getEndTime());
            // 将seckillSpuVO对象保存到Redis以便后续请求直接从Redis获取
            redisTemplate.boundValueOps(spuVOKey).set(
                    seckillSpuVO,
                    1000*60*5 + RandomUtils.nextInt(10000),
                    TimeUnit.MILLISECONDS);
        }
        // 到此为止,seckillSpuVO对象只有url属性没有赋值了
        // url属性响应给前端,前端使用这个路径来提交表单,才有正确的随机码
        // 需要先判断,当前时间是否是允许秒杀购买当前商品的时间段
        // 获取当前时间
        LocalDateTime nowTime=LocalDateTime.now();
        // 对比时为了减少系统消耗,尽量不连接数据库,直接使用seckillSpuVO中的开始和结束时间
        // 判断逻辑是秒杀开始时间小于当前时间,并且当前时间小于秒杀结束时间
        if(seckillSpuVO.getStartTime().isBefore(nowTime) &&
                nowTime.isBefore(seckillSpuVO.getEndTime())){
            // 进入if表示当前时间是允许对当前商品进行秒杀购买操作的,可以为url赋值
            // 我们要从Redis中获取已经预热的随机码
            String randCodeKey=SeckillCacheUtils.getRandCodeKey(spuId);
            // 判断随机码是否已经在Redis中
            if(! redisTemplate.hasKey(randCodeKey)){
                // 如果不存在,直接抛异常
                throw new CoolSharkServiceException(
                        ResponseCode.INTERNAL_SERVER_ERROR,"当前随机码不存在(等秒针到0再试)");
            }
            // key如果正常,就从Redis中获取
            String randCode=redisTemplate.boundValueOps(randCodeKey).get()+"";
            // 随机码赋值到url属性中
            seckillSpuVO.setUrl("/seckill/"+randCode);
            log.info("商品详情对象构建完成,url属性为:{}",seckillSpuVO.getUrl());
        }
        // 最后一定要将seckillSpuVO返回
        return seckillSpuVO;
    }

    // 项目中没有给定SpuDetail用Key的常量,所以我们自己定义一个
    public static final String
            SECKILL_SPU_DETAIL_VO_PREFIX="seckill:spu:detail:vo:";
    // 根据spuId查询秒杀用SpuDetail
    @Override
    public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
        // 先获取操作Redis的key
        String spuDetailKey=SECKILL_SPU_DETAIL_VO_PREFIX+spuId;
        // 先声明返回值类型的对象,赋值为null
        SeckillSpuDetailSimpleVO simpleVO=null;
        // 判断Redis中是否已经有key
        if(redisTemplate.hasKey(spuDetailKey)){
            // 如果存在,直接从redis中取
            simpleVO= (SeckillSpuDetailSimpleVO)
                    redisTemplate.boundValueOps(spuDetailKey).get();
        }else{
            // Redis中没有这个key
            // 要利用Dubbo从product模块查询SpuDetail对象
            SpuDetailStandardVO spuDetailStandardVO =
                    dubboSeckillSpuService.getSpuDetailById(spuId);
            // 先实例化simpleVO,使它非null
            simpleVO=new SeckillSpuDetailSimpleVO();
            BeanUtils.copyProperties(spuDetailStandardVO,simpleVO);
            //  保存到Redis中
            redisTemplate.boundValueOps(spuDetailKey).set(
                    simpleVO,
                    1000*60*5+RandomUtils.nextInt(10000),
                    TimeUnit.MILLISECONDS);
        }
        // 最后别忘了返回!!!
        return simpleVO;
    }
}







